package com.aldobucchi.tree
{
	
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	
	import mx.collections.IList;
	import mx.events.CollectionEvent;
	
	import com.aldobucchi.utils.AutoUpdateFlag;
	import com.aldobucchi.x.mx.collections.IList.IListUtil;




/**
 *  
 * 
 *  @eventType com.aldobucchi.tree.TreeEvent.PARENT_CHANGED
 */
[Event(name="parentChanged", type="com.aldobucchi.tree.TreeEvent")]		




/**
 *  
 * 
 *  @eventType com.aldobucchi.tree.TreeEvent.TOP_CHANGED
 */
[Event(name="topChanged", type="com.aldobucchi.tree.TreeEvent")]



/**
 *  
 * 
 *  @eventType com.aldobucchi.tree.TreeEvent.DATA_CHANGED
 */
[Event(name="dataChanged", type="com.aldobucchi.tree.TreeEvent")]





/**
 *  
 * 
 *  @eventType com.aldobucchi.tree.TreeEvent.CONFIG_CHANGED
 */
[Event(name="configChanged", type="com.aldobucchi.tree.TreeEvent")]




/**
 *  
 * 
 *  @eventType com.aldobucchi.tree.TreeEvent.TREE_CHANGED
 */
[Event(name="treeChanged", type="com.aldobucchi.tree.TreeEvent")]









	/**
	 * 
	 * 
	 * @author aldo.bucchi@gmail.com
	 * 
	 */
	public class TreeNode extends EventDispatcher
	{
		
		
		
		private var _autoUpdateFlag:AutoUpdateFlag = new AutoUpdateFlag( null, enabledChanged );
		public function get autoUpdateFlag( ):AutoUpdateFlag
		{
			return _autoUpdateFlag;
		}
		
		
		
		

		////////////////////////////////////////////////////
		// parent
		////////////////////////////////////////////////////
		
		private var _parent:TreeNode;
		[Bindable("parentChanged")]
		public function get parent( ):TreeNode
		{
			return _parent;		
		}
		
		
		
		

		////////////////////////////////////////////////////
		// children
		////////////////////////////////////////////////////
		
		private var _children:TreeChildrenList;
		[Bindable("childrenChanged")] // will never change, this is just here to avoid binding warnings
		public function get children( ):TreeChildrenList
		{
			if ( _children == null ) // lazy instantiation... might help a bit with memory
			{
				_children = new TreeChildrenList( this  );		
				_children.addEventListener( CollectionEvent.COLLECTION_CHANGE, children_change, false, 0, true );			
			}
			return _children;
		}
		



		////////////////////////////////////////////////////
		// top
		////////////////////////////////////////////////////
		
		[Bindable("topChanged")]
		public function get top( ):TreeNode
		{
			
			if ( parent == null )
			{
				return this;			
			}
			return parent.top;

		}


		
		////////////////////////////////////////////////////
		// data
		////////////////////////////////////////////////////
		
		private var _data:*;
		[Bindable("dataChanged")]
		public function get data( ):*
		{
			return _data;		
		}
		public function set data( v:* ):void
		{
			if ( _data == v )
				return;
				
			_data = v;
			
			dispatchEvent( new TreeEvent( TreeEvent.DATA_CHANGED ) );
		}






		////////////////////////////////////////////////////
		// config
		////////////////////////////////////////////////////
		
		private var _config:TreeConfig;
		[Bindable("configChanged")]
		public function get config( ):TreeConfig
		{
			return _config;		
		}


		








		////////////////////////////////////////////////////
		// constructor
		////////////////////////////////////////////////////
		
		public function TreeNode( data:*=undefined )
		{
		
			_data = data;
			

			
		}
		






		/**
		 * 
		 * @param toNode
		 * @return 
		 * 		<ul>
		 * 			<li>NULL if nodes are not related or are equal</li>
		 * 			<li>An empty array if they have a parent-child relation</li>
		 * 			<li>An array with all the nodes found between this and toNode ( in the correct order )</li>
 		 * 		<ul>
		 * 		
		 * 
		 */
		public function getIntermediateNodes( toNode:TreeNode ):Array
		{
		
			if ( toNode == null )
				return null;
				
			if ( toNode == this )
				return null;
				
		
			if ( hasAncestor( toNode ) )
			{
				var arr	:Array = [ ];
				var p	:TreeNode = parent;
				
				while ( p != toNode )
				{
					arr.push( p );			
					p = p.parent;
				}

				return arr;

			}
			else if ( hasDescendant( toNode ) )
			{
				// it is faster to look upstream
				var res:Array =  toNode.getIntermediateNodes( this )
				res = res.reverse();
				return res;

			}
			
			return null;
		
		}










		
		/**
		 * 
		 * 
		 * 
		 * @param ancestor
		 * @return 
		 * 		true if the passed in node is an ancestor of this one.
		 * 		Note: If you pass this very same node, the result will be false
		 * 
		 */		
		public function hasAncestor( ancestor:TreeNode ):Boolean
		{
			
			if ( ancestor == this )
				return false;
				
			if ( parent == null )
				return false;
				
			if ( parent == ancestor )
				return true;
				
			return parent.hasAncestor( ancestor );
					
		}





		
		public function hasDescendant( descendant:TreeNode ):Boolean
		{
			if ( descendant == this )
				return false;
				
			if ( _children == null )
				return false;
				
			for each ( var child:TreeNode in _children.toArray( ) )
			{
				if ( child == descendant )
					return true;					
			
				if ( child.hasDescendant( descendant ) )
					return true;
			}
			
			return false;
			
		}




		
		
		/**
		 * 
		 * Removes this node from our parent
		 * 
		 */		
		public function remove( ):Boolean
		{
		
			if ( this.parent == null )
				return false;
			
			return parent.children.removeItem( this );
		
		}
		
		
		
		
		/**
		 * 
		 * Removes us from our parent, and puts our children in our place
		 * 
		 */		
		public function collapse( ):Boolean
		{
		
			if ( parent.children.removeItem( this ) )
			{
				for each ( var child:TreeNode in children.toArray( ) )
				{
					parent.children.addItem( child );
				}
				
				return true;
			}
			
			return false;
			
		}
		
		
		
		
		/////////////////////////////////////////////////////
		// cloning
		/////////////////////////////////////////////////////
		
		
		private  var _cloneOf:TreeNode;

		public function get cloneOf( ):TreeNode
		{
			return _cloneOf;
		}



		/**
		 * 
		 * we use weak keys for memory considerations.
		 * We want the clones to remember us forever, but not the other way around
		 * 
		 */		
		private var clones:Dictionary = new Dictionary( true );
		
		
		/**
		 * 
		 * 
		 * 
		 * @param stopAtNodes
		 * 		An array containing stop nodes...their clones WILL be added to the clone tree but their children's clones will not.
		 * 	
		 * @return 
		 * 
		 */		
		public function deepClone( stopAtNodes:Array = null ):TreeNode
		{
			
			var thisClone:TreeNode = clone( );
			var c:IList = thisClone.children;
			
			IListUtil.autoUpdate( c, false );
			
			for each ( var child:TreeNode in children.toArray( ) )
			{
				if ( stopAtNodes != null && ( stopAtNodes.indexOf( child ) != -1 ) )
				{
					c.addItem( child.clone( ) );					
				}
				else
				{
					c.addItem( child.deepClone( stopAtNodes ) );
				}
			}
			
			IListUtil.autoUpdate( c, true );			
			
			return thisClone;	
		}
		
		
		protected function shallowClone( ):TreeNode
		{
			return new TreeNode( data );
		}
		
		
		
		public final function clone( ):TreeNode
		{
			var tn:TreeNode = shallowClone( );
			
			tn._cloneOf 	= this;
			clones[ tn ] 	= true;
			
			return tn;
		}		
		
		
		
		
		
		public function findCloneInOtherTree( node:TreeNode ):TreeNode
		{
		
			if ( node.cloneOf == this )
				return node;
				
			var clone:TreeNode = null;	
				
			for each ( var child:TreeNode in node.children.toArray() )
				if ( ( clone = findCloneInOtherTree( child ) ) != null )
					return clone;
		
			return null;
		}
		
		
		
		
		
		
		
		






		/////////////////////////////////////////////
		// keeping parent-child relationship updated
		/////////////////////////////////////////////


		
		private function children_change( event:CollectionEvent ):void
		{
			
			// update parent on children
			for each ( var item:* in event.items )
				if ( item is TreeNode ) // item can also be an object for some event kinds
					updateParentOnChild( item as TreeNode );
					
			
			bubbleChange( );
			
		}
		
		
		


		private function updateParentOnChild( node:TreeNode ):void
		{
			
			var isOurChild:Boolean		= false;
			
			if ( _children != null )
				isOurChild = ( _children.getItemIndex( node ) != -1 );
				
						
			var weAreHisParent:Boolean	= ( node.parent == this );
		
			// make sure we are on the same page
			
			if ( isOurChild && weAreHisParent ) // OK
				return; 
			
			node.setParent( isOurChild ? this : null );
			
			
		
		}
				
		
		
		
		
		
		
		
		
		private var bubblePendingFlag:Boolean = false;
		
		private function enabledChanged( enabled:Boolean ):void
		{
			if ( bubblePendingFlag && enabled )
				bubbleChange( );
		}
		
		
		
		
		
		
		
		
		
		////////////////////////////////////////////
		// Events
		////////////////////////////////////////////
		

		/**
		 * 
		 * upstream
		 * 
		 */		
		protected function bubbleChange( ):void
		{
			if ( autoUpdateFlag.isEnabled )
			{
				$bubbleChange( );			
				bubblePendingFlag = false;
			}
			else
				bubblePendingFlag = true;
		}
		
		
		
		protected function $bubbleChange( ):void
		{
			if ( parent != null )
				parent.bubbleChange( );

			else
				informChange( );
		}



		/**
		 * 
		 * downstream
		 * 
		 */
		internal function informChange( ):void
		{

			dispatchEvent( new TreeEvent( TreeEvent.TREE_CHANGED ) );

			if ( _children == null )
				return;

			for each ( var childNode:TreeNode in _children.toArray() )
			{
				childNode.informChange( );
			}

		}
		
		
		
		
		internal function setParent( newParent:TreeNode ):void
		{
			if ( _parent == newParent )
				return;
			
			
			if ( _parent ) // remove from previous parent if necessary
			{
				_parent.children.removeItem( this );
			}
			
			
			_parent = newParent;
			
			dispatchEvent( new TreeEvent( TreeEvent.PARENT_CHANGED ) );		
		
		}
				
		
		
		
		
		
		
		
		
		
		/**
		 * 
		 * Convenience function used to build a hierarchical structure from an array
		 * <pre>
		 * 
		 * [ root, [child1], [child2, [grandChild1] ] ]
		 * 
		 * 
		 * </pre>
		 * 
		 * @param arr
		 * @return 
		 * 
		 */		
		public static function buildTree( arr:Array ):TreeNode
		{
		
			var node:TreeNode = arr[ 0 ] as TreeNode;
		
			for ( var i:int=1; i<arr.length; i++ )
				node.children.addItem( buildTree( arr[i] ) );			

			return node;		
		
		}
		
		
		
		
		
		
		
		
		
	}
}